Skip to content

Use correct to_remote script in counterparty commitments #2605

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

wpaulino
Copy link
Contributor

While our commitment transactions did use the correct to_remote script, the ChannelMonitor's was not as it is tracked separately. This would lead to users never receiving an Event::SpendableOutputs with a StaticPaymentOutput descriptor to claim the funds.

Luckily, any users affected which had channel closures confirmed by a counterparty commitment just need to replay the closing transaction to receive the event.

@wpaulino wpaulino added this to the 0.0.117 milestone Sep 26, 2023
@codecov-commenter
Copy link

codecov-commenter commented Sep 27, 2023

Codecov Report

Attention: 23 lines in your changes are missing coverage. Please review.

Comparison is base (6016101) 89.03% compared to head (f464aa9) 88.99%.
Report is 20 commits behind head on main.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2605      +/-   ##
==========================================
- Coverage   89.03%   88.99%   -0.04%     
==========================================
  Files         112      112              
  Lines       86351    86706     +355     
  Branches    86351    86706     +355     
==========================================
+ Hits        76879    77167     +288     
- Misses       7235     7308      +73     
+ Partials     2237     2231       -6     
Files Coverage Δ
lightning/src/ln/monitor_tests.rs 98.30% <100.00%> (-0.04%) ⬇️
lightning/src/util/test_utils.rs 77.06% <100.00%> (+0.10%) ⬆️
lightning/src/events/bump_transaction.rs 82.33% <0.00%> (ø)
lightning/src/ln/chan_utils.rs 92.09% <62.50%> (+0.02%) ⬆️
lightning/src/chain/channelmonitor.rs 84.86% <73.91%> (-0.02%) ⬇️
lightning/src/sign/mod.rs 74.05% <73.91%> (-0.36%) ⬇️

... and 11 files with indirect coverage changes

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@wpaulino wpaulino force-pushed the anchors-monitor-track-to-remote-script branch 3 times, most recently from f6b21c9 to 1fb2a41 Compare September 28, 2023 16:42
@TheBlueMatt
Copy link
Collaborator

The commit titled Fix off-by-one max witness estimate for P2WPKH StaticPaymentDescriptor isn't fixing anything - its replacing 1 + 73 + 34 with 1 + 73 + 1 + 33, which are the same :). Its a nice cleanup to use a constant, though.

@wpaulino
Copy link
Contributor Author

The commit titled Fix off-by-one max witness estimate for P2WPKH StaticPaymentDescriptor isn't fixing anything - its replacing 1 + 73 + 34 with 1 + 73 + 1 + 33, which are the same :). Its a nice cleanup to use a constant, though.

It is, check again :) We previously had an estimate of 108, now 109.

@wpaulino wpaulino force-pushed the anchors-monitor-track-to-remote-script branch from 1fb2a41 to 9113ea3 Compare September 28, 2023 19:33
@TheBlueMatt
Copy link
Collaborator

Oh lol the extra one I skimmed past.

Comment on lines -894 to +972
let remotepubkey = self.pubkeys().payment_point;
let witness_script = bitcoin::Address::p2pkh(&::bitcoin::PublicKey{compressed: true, inner: remotepubkey}, Network::Testnet).script_pubkey();
let remotepubkey = bitcoin::PublicKey::new(self.pubkeys().payment_point);
let witness_script = if self.channel_type_features().supports_anchors_zero_fee_htlc_tx() {
chan_utils::get_to_countersignatory_with_anchors_redeemscript(&remotepubkey.inner)
} else {
Script::new_p2pkh(&remotepubkey.pubkey_hash())
};
let sighash = hash_to_message!(&sighash::SighashCache::new(spend_tx).segwit_signature_hash(input_idx, &witness_script, descriptor.output.value, EcdsaSighashType::All).unwrap()[..]);
let remotesig = sign_with_aux_rand(secp_ctx, &sighash, &self.payment_key, &self);
let payment_script = bitcoin::Address::p2wpkh(&::bitcoin::PublicKey{compressed: true, inner: remotepubkey}, Network::Bitcoin).unwrap().script_pubkey();
let payment_script = if self.channel_type_features().supports_anchors_zero_fee_htlc_tx() {
witness_script.to_v0_p2wsh()
} else {
Script::new_v0_p2wpkh(&remotepubkey.wpubkey_hash().unwrap())
};

if payment_script != descriptor.output.script_pubkey { return Err(()); }

let mut witness = Vec::with_capacity(2);
witness.push(remotesig.serialize_der().to_vec());
witness[0].push(EcdsaSighashType::All as u8);
witness.push(remotepubkey.serialize().to_vec());
if self.channel_type_features().supports_anchors_zero_fee_htlc_tx() {
witness.push(witness_script.to_bytes());
} else {
witness.push(remotepubkey.to_bytes());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We condition on this quite a bit. Wonder if it would be worth type parameterizing to avoid missing places in the future. Doesn't need to be done now, of course.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind giving a more concrete suggestion?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will change even further once we have taproot and we need different sighash and signing methods.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that rather than checking against features, you initialized the signer with a parameterization for the channel type. Then instead of having an increasingly complex condition for each of these three assignments, each has it's own implementation. The strategy pattern, essentially. Though, in practice this parameterization may need to trickle up to ChannelMonitor and possibly further, so might not be worth it.

Alternatively, could just use an enum as mentioned later in another comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that rather than checking against features, you initialized the signer with a parameterization for the channel type. Then instead of having an increasingly complex condition for each of these three assignments, each has it's own implementation. The strategy pattern, essentially. Though, in practice this parameterization may need to trickle up to ChannelMonitor and possibly further, so might not be worth it.

This would be quite a big change if we intend to apply it to all other signing methods on InMemorySigner, since it wouldn't make sense to have it on some but not others.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea though, especially as we add taproot and further channel/commitment upgrades.

Comment on lines +329 to +344
let sequence =
if descriptor.channel_transaction_parameters.as_ref()
.map(|channel_params| channel_params.channel_type_features.supports_anchors_zero_fee_htlc_tx())
.unwrap_or(false)
{
Sequence::from_consensus(1)
} else {
Sequence::ZERO
};
input.push(TxIn {
previous_output: descriptor.outpoint.into_bitcoin_outpoint(),
script_sig: Script::new(),
sequence: Sequence::ZERO,
sequence,
witness: Witness::new(),
});
witness_weight += StaticPaymentOutputDescriptor::MAX_WITNESS_LENGTH;
witness_weight += descriptor.max_witness_length();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... I'd definitely lean towards a separate variant, FWIW. Seems cleaner.

Copy link
Contributor Author

@wpaulino wpaulino Sep 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thought of adding a new variant came from a confusion between StaticOutput and StaticPaymentOutput descriptors. Once we upgrade to Taproot, we'd also need a new variant because we're going from P2WSH to P2TR. Not sure it's worth having all these different types when it's still the same output (to_remote on a counterparty commitment) we're claiming backed by the same key.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. My thinking was that by explicitly enumerating the cases, we would be forced to consider all of them and avoid bugs. I suppose an alternative would be to have a mapping from ChannelTypeFeatures to an enum with a variant for each type that we could match on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see if I can come up with something tomorrow, it'll have to wait for when taproot is added here otherwise. In any case, the best way to avoid bugs here would be test coverage via check_spends!() 😛

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM. A mapping wouldn't require any serialization changes, too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to expand on this based on offline discussion. We'll keep the variants as they are in this PR. This will give us a ChannelTransactionParameters which has a ChannelTypeFeatures. Later, we can add something like a to_channel_type method to ChannelTypeFeatures for internal use that will give an enum for us to match so as to avoid missing cases.

Using either another variant or a nested enum instead could be error prone as it may go out of sync with ChannelTypeFeatures.

@wpaulino wpaulino force-pushed the anchors-monitor-track-to-remote-script branch from 9113ea3 to 163ad99 Compare September 28, 2023 22:47
Copy link

@ariard ariard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code sounds good.

if let Event::SpendableOutputs { outputs, channel_id } = event {
assert_eq!(outputs.len(), 1);
assert!(vec![chan_b.2, chan_a.2].contains(&channel_id.unwrap()));
let spend_tx = nodes[0].keys_manager.backing.spend_spendable_outputs(
&[&outputs[0]], Vec::new(), Script::new_op_return(&[]), 253, None, &Secp256k1::new(),
).unwrap();

check_spends!(spend_tx, revoked_claim_transactions.get(&spend_tx.input[0].previous_output.txid).unwrap());
if let SpendableOutputDescriptor::StaticPaymentOutput(_) = &outputs[0] {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think one way to harden the test can be to count the StaticPaymentOutput with num_static_payment_output and check at the end the right number of this type of descriptor has been found.

jkczyz
jkczyz previously approved these changes Sep 29, 2023
While our commitment transactions did use the correct `to_remote`
script, the `ChannelMonitor`'s was not as it is tracked separately. This
would lead to users never receiving an `Event::SpendableOutputs` with a
`StaticPaymentOutput` descriptor to claim the funds.

Luckily, any users affected which had channel closures confirmed by a
counterparty commitment just need to replay the closing transaction to
receive the event.
`to_remote` outputs on commitment transactions with anchor outputs have
an additional `1 CSV` constraint on its spending condition,
transitioning away from the previous P2WPKH script to a P2WSH.

Since our `ChannelMonitor` was never updated to track the proper
`to_remote` script on anchor outputs channels, we also missed updating
our signer to handle the new script changes.
We were not accounting for the extra byte denoting the number of items
in the witness stack.
@wpaulino wpaulino force-pushed the anchors-monitor-track-to-remote-script branch from 163ad99 to 5607977 Compare September 29, 2023 20:47
@wpaulino
Copy link
Contributor Author

wpaulino commented Sep 29, 2023

Had to rebase to use get_spendable_outputs in the new test so I used fixups this time around to better show what changed.

@jkczyz
Copy link
Contributor

jkczyz commented Sep 29, 2023

Thanks for adding the test! LGTM. Fee free to squash the fixups.

@wpaulino wpaulino force-pushed the anchors-monitor-track-to-remote-script branch from 5607977 to f464aa9 Compare September 29, 2023 21:22
@TheBlueMatt TheBlueMatt merged commit 620244d into lightningdevkit:main Sep 29, 2023
@wpaulino wpaulino deleted the anchors-monitor-track-to-remote-script branch September 29, 2023 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants